//
//  Copyright (c) 2021 Open Whisper Systems. All rights reserved.
//

#import "TSOutgoingMessage.h"
#import "MessageSender.h"
#import "NSError+OWSOperation.h"
#import "OWSContact.h"
#import "OWSOutgoingSyncMessage.h"
#import "ProtoUtils.h"
#import "SSKEnvironment.h"
#import "SignalRecipient.h"
#import "TSAccountManager.h"
#import "TSAttachmentStream.h"
#import "TSContactThread.h"
#import "TSGroupThread.h"
#import "TSQuotedMessage.h"
#import <SignalCoreKit/NSDate+OWS.h>
#import <SignalCoreKit/NSString+OWS.h>
#import <SignalServiceKit/AppReadiness.h>
#import <SignalServiceKit/SignalServiceKit-Swift.h>

NS_ASSUME_NONNULL_BEGIN

typedef NS_CLOSED_ENUM(NSUInteger, OutgoingGroupProtoResult) { OutgoingGroupProtoResult_AddedWithGroupAvatar,
    OutgoingGroupProtoResult_AddedWithoutGroupAvatar,
    OutgoingGroupProtoResult_Error };

NSString *const kTSOutgoingMessageSentRecipientAll = @"kTSOutgoingMessageSentRecipientAll";

NSString *NSStringForOutgoingMessageState(TSOutgoingMessageState value)
{
    switch (value) {
        case TSOutgoingMessageStateSending:
            return @"TSOutgoingMessageStateSending";
        case TSOutgoingMessageStateFailed:
            return @"TSOutgoingMessageStateFailed";
        case TSOutgoingMessageStateSent_OBSOLETE:
            return @"TSOutgoingMessageStateSent_OBSOLETE";
        case TSOutgoingMessageStateDelivered_OBSOLETE:
            return @"TSOutgoingMessageStateDelivered_OBSOLETE";
        case TSOutgoingMessageStateSent:
            return @"TSOutgoingMessageStateSent";
        case TSOutgoingMessageStatePending:
            return @"TSOutgoingMessageStatePending";
    }
}

NSString *NSStringForOutgoingMessageRecipientState(OWSOutgoingMessageRecipientState value)
{
    switch (value) {
        case OWSOutgoingMessageRecipientStateFailed:
            return @"OWSOutgoingMessageRecipientStateFailed";
        case OWSOutgoingMessageRecipientStateSending:
            return @"OWSOutgoingMessageRecipientStateSending";
        case OWSOutgoingMessageRecipientStateSkipped:
            return @"OWSOutgoingMessageRecipientStateSkipped";
        case OWSOutgoingMessageRecipientStateSent:
            return @"OWSOutgoingMessageRecipientStateSent";
        case OWSOutgoingMessageRecipientStatePending:
            return @"OWSOutgoingMessageRecipientStatePending";
    }
}

#pragma mark -

@interface TSMessage (Private)

- (void)removeAllAttachmentsWithTransaction:(SDSAnyWriteTransaction *)transaction;

@end

#pragma mark -

@interface TSOutgoingMessageRecipientState ()

@property (atomic) OWSOutgoingMessageRecipientState state;
@property (atomic, nullable) NSNumber *deliveryTimestamp;
@property (atomic, nullable) NSNumber *readTimestamp;
@property (atomic, nullable) NSNumber *viewedTimestamp;
@property (atomic, nullable) NSNumber *errorCode;
@property (atomic) BOOL wasSentByUD;

@end

#pragma mark -

@implementation TSOutgoingMessageRecipientState

@end

#pragma mark -

NSUInteger const TSOutgoingMessageSchemaVersion = 1;

@interface TSOutgoingMessage ()

@property (atomic) BOOL hasSyncedTranscript;
@property (atomic, nullable) NSString *customMessage;
@property (atomic, nullable) NSString *mostRecentFailureText;
@property (atomic) BOOL isFromLinkedDevice;
@property (atomic) TSGroupMetaMessage groupMetaMessage;
@property (nonatomic, readonly) NSUInteger outgoingMessageSchemaVersion;

@property (nonatomic, readonly) TSOutgoingMessageState legacyMessageState;
@property (nonatomic, readonly) BOOL legacyWasDelivered;
@property (nonatomic, readonly) BOOL hasLegacyMessageState;
@property (atomic, nullable)
    NSDictionary<SignalServiceAddress *, TSOutgoingMessageRecipientState *> *recipientAddressStates;

// This property is only intended to be used by GRDB queries.
@property (nonatomic, readonly) TSOutgoingMessageState storedMessageState;

@end

#pragma mark -

@implementation TSOutgoingMessage

// --- CODE GENERATION MARKER

// This snippet is generated by /Scripts/sds_codegen/sds_generate.py. Do not manually edit it, instead run `sds_codegen.sh`.

// clang-format off

- (instancetype)initWithGrdbId:(int64_t)grdbId
                      uniqueId:(NSString *)uniqueId
             receivedAtTimestamp:(uint64_t)receivedAtTimestamp
                          sortId:(uint64_t)sortId
                       timestamp:(uint64_t)timestamp
                  uniqueThreadId:(NSString *)uniqueThreadId
                   attachmentIds:(NSArray<NSString *> *)attachmentIds
                            body:(nullable NSString *)body
                      bodyRanges:(nullable MessageBodyRanges *)bodyRanges
                    contactShare:(nullable OWSContact *)contactShare
                 expireStartedAt:(uint64_t)expireStartedAt
                       expiresAt:(uint64_t)expiresAt
                expiresInSeconds:(unsigned int)expiresInSeconds
              isViewOnceComplete:(BOOL)isViewOnceComplete
               isViewOnceMessage:(BOOL)isViewOnceMessage
                     linkPreview:(nullable OWSLinkPreview *)linkPreview
                  messageSticker:(nullable MessageSticker *)messageSticker
                   quotedMessage:(nullable TSQuotedMessage *)quotedMessage
    storedShouldStartExpireTimer:(BOOL)storedShouldStartExpireTimer
              wasRemotelyDeleted:(BOOL)wasRemotelyDeleted
                   customMessage:(nullable NSString *)customMessage
                groupMetaMessage:(TSGroupMetaMessage)groupMetaMessage
           hasLegacyMessageState:(BOOL)hasLegacyMessageState
             hasSyncedTranscript:(BOOL)hasSyncedTranscript
              isFromLinkedDevice:(BOOL)isFromLinkedDevice
                  isVoiceMessage:(BOOL)isVoiceMessage
              legacyMessageState:(TSOutgoingMessageState)legacyMessageState
              legacyWasDelivered:(BOOL)legacyWasDelivered
           mostRecentFailureText:(nullable NSString *)mostRecentFailureText
          recipientAddressStates:(nullable NSDictionary<SignalServiceAddress *,TSOutgoingMessageRecipientState *> *)recipientAddressStates
              storedMessageState:(TSOutgoingMessageState)storedMessageState
{
    self = [super initWithGrdbId:grdbId
                        uniqueId:uniqueId
               receivedAtTimestamp:receivedAtTimestamp
                            sortId:sortId
                         timestamp:timestamp
                    uniqueThreadId:uniqueThreadId
                     attachmentIds:attachmentIds
                              body:body
                        bodyRanges:bodyRanges
                      contactShare:contactShare
                   expireStartedAt:expireStartedAt
                         expiresAt:expiresAt
                  expiresInSeconds:expiresInSeconds
                isViewOnceComplete:isViewOnceComplete
                 isViewOnceMessage:isViewOnceMessage
                       linkPreview:linkPreview
                    messageSticker:messageSticker
                     quotedMessage:quotedMessage
      storedShouldStartExpireTimer:storedShouldStartExpireTimer
                wasRemotelyDeleted:wasRemotelyDeleted];

    if (!self) {
        return self;
    }

    _customMessage = customMessage;
    _groupMetaMessage = groupMetaMessage;
    _hasLegacyMessageState = hasLegacyMessageState;
    _hasSyncedTranscript = hasSyncedTranscript;
    _isFromLinkedDevice = isFromLinkedDevice;
    _isVoiceMessage = isVoiceMessage;
    _legacyMessageState = legacyMessageState;
    _legacyWasDelivered = legacyWasDelivered;
    _mostRecentFailureText = mostRecentFailureText;
    _recipientAddressStates = recipientAddressStates;
    _storedMessageState = storedMessageState;

    return self;
}

// clang-format on

// --- CODE GENERATION MARKER

- (nullable instancetype)initWithCoder:(NSCoder *)coder
{
    self = [super initWithCoder:coder];

    if (self) {
        OWSAssertDebug(self.outgoingMessageSchemaVersion >= 1);

        _outgoingMessageSchemaVersion = TSOutgoingMessageSchemaVersion;
    }


    return self;
}

+ (instancetype)outgoingMessageInThread:(TSThread *)thread
                            messageBody:(nullable NSString *)body
                           attachmentId:(nullable NSString *)attachmentId
{
    return [self outgoingMessageInThread:thread
                             messageBody:body
                            attachmentId:attachmentId
                        expiresInSeconds:0
                           quotedMessage:nil
                             linkPreview:nil
                          messageSticker:nil];
}

+ (instancetype)outgoingMessageInThread:(TSThread *)thread
                            messageBody:(nullable NSString *)body
                           attachmentId:(nullable NSString *)attachmentId
                       expiresInSeconds:(uint32_t)expiresInSeconds
{
    return [self outgoingMessageInThread:thread
                             messageBody:body
                            attachmentId:attachmentId
                        expiresInSeconds:expiresInSeconds
                           quotedMessage:nil
                             linkPreview:nil
                          messageSticker:nil];
}

+ (instancetype)outgoingMessageInThread:(TSThread *)thread
                            messageBody:(nullable NSString *)body
                           attachmentId:(nullable NSString *)attachmentId
                       expiresInSeconds:(uint32_t)expiresInSeconds
                          quotedMessage:(nullable TSQuotedMessage *)quotedMessage
                            linkPreview:(nullable OWSLinkPreview *)linkPreview
                         messageSticker:(nullable MessageSticker *)messageSticker
{
    NSMutableArray<NSString *> *attachmentIds = [NSMutableArray new];
    if (attachmentId) {
        [attachmentIds addObject:attachmentId];
    }

    TSOutgoingMessageBuilder *builder = [TSOutgoingMessageBuilder outgoingMessageBuilderWithThread:thread];
    builder.messageBody = body;
    builder.attachmentIds = attachmentIds;
    builder.expiresInSeconds = expiresInSeconds;
    builder.quotedMessage = quotedMessage;
    builder.linkPreview = linkPreview;
    builder.messageSticker = messageSticker;
    return [builder build];
}

+ (instancetype)outgoingMessageInThread:(TSThread *)thread
                       groupMetaMessage:(TSGroupMetaMessage)groupMetaMessage
                       expiresInSeconds:(uint32_t)expiresInSeconds
{
    TSOutgoingMessageBuilder *builder = [TSOutgoingMessageBuilder outgoingMessageBuilderWithThread:thread];
    builder.groupMetaMessage = groupMetaMessage;
    builder.expiresInSeconds = expiresInSeconds;
    return [builder build];
}

+ (instancetype)outgoingMessageInThread:(TSThread *)thread
                       groupMetaMessage:(TSGroupMetaMessage)groupMetaMessage
                       expiresInSeconds:(uint32_t)expiresInSeconds
                 changeActionsProtoData:(nullable NSData *)changeActionsProtoData
{
    TSOutgoingMessageBuilder *builder = [TSOutgoingMessageBuilder outgoingMessageBuilderWithThread:thread];
    builder.groupMetaMessage = groupMetaMessage;
    builder.expiresInSeconds = expiresInSeconds;
    builder.changeActionsProtoData = changeActionsProtoData;
    return [builder build];
}

- (instancetype)initOutgoingMessageWithBuilder:(TSOutgoingMessageBuilder *)outgoingMessageBuilder
{
    self = [super initMessageWithBuilder:outgoingMessageBuilder];
    if (!self) {
        return self;
    }

    _hasSyncedTranscript = NO;

    TSThread *thread = outgoingMessageBuilder.thread;
    TSGroupMetaMessage groupMetaMessage = outgoingMessageBuilder.groupMetaMessage;

    if ([thread isKindOfClass:TSGroupThread.class]) {
        // Unless specified, we assume group messages are "Delivery" i.e. normal messages.
        if (groupMetaMessage == TSGroupMetaMessageUnspecified) {
            _groupMetaMessage = TSGroupMetaMessageDeliver;
        } else {
            _groupMetaMessage = groupMetaMessage;
        }
    } else {
        OWSAssertDebug(groupMetaMessage == TSGroupMetaMessageUnspecified);
        // Specifying a group meta message only makes sense for Group threads
        _groupMetaMessage = TSGroupMetaMessageUnspecified;
    }

    _isVoiceMessage = outgoingMessageBuilder.isVoiceMessage;

    // New outgoing messages should immediately determine their
    // recipient list from current thread state.
    NSMutableSet<SignalServiceAddress *> *recipientAddresses = [NSMutableSet new];
    if ([self isKindOfClass:[OWSOutgoingSyncMessage class]]) {
        // 1. Sync messages should only be sent to linked devices.
        OWSAssertDebug(TSAccountManager.localAddress);
        [recipientAddresses addObject:TSAccountManager.localAddress];
    } else {
        // 2. Most messages should only be sent to the current members of the group.
        [recipientAddresses addObjectsFromArray:thread.recipientAddresses];
        // 3. V2 group updates should also be sent to pending members of the group
        //    whose clients support Groups v2.
        if (outgoingMessageBuilder.additionalRecipients) {
            [recipientAddresses addObjectsFromArray:outgoingMessageBuilder.additionalRecipients];
        }
    }

    NSMutableDictionary<SignalServiceAddress *, TSOutgoingMessageRecipientState *> *recipientAddressStates =
        [NSMutableDictionary new];
    for (SignalServiceAddress *recipientAddress in recipientAddresses) {
        if (!recipientAddress.isValid) {
            OWSFailDebug(@"Ignoring invalid address.");
            continue;
        }
        TSOutgoingMessageRecipientState *recipientState = [TSOutgoingMessageRecipientState new];
        recipientState.state = OWSOutgoingMessageRecipientStateSending;
        recipientAddressStates[recipientAddress] = recipientState;
    }
    self.recipientAddressStates = [recipientAddressStates copy];
    _outgoingMessageSchemaVersion = TSOutgoingMessageSchemaVersion;

    _changeActionsProtoData = outgoingMessageBuilder.changeActionsProtoData;

    return self;
}

// Each message has the responsibility for eagerly cleaning up its attachments.
// Normally this is done in [TSMessage removeWithTransaction], but that doesn't
// apply for "transient", unsaved messages (i.e. shouldBeSaved == NO).  These
// messages need to be cleaned up explicitly.
- (void)removeTemporaryAttachmentsWithTransaction:(SDSAnyWriteTransaction *)transaction
{
    if (self.shouldBeSaved) {
        // Message is not transient; no need to clean up attachments.
        OWSFailDebug(@"Message is not transient.");
        return;
    }
    if (!AppReadiness.isAppReady) {
        // We don't want or need to do this clean up while registering extensions,
        // migrating, etc.
        OWSFailDebug(@"App is not ready.");
        return;
    }
    NSArray<NSString *> *_Nullable attachmentIds = self.attachmentIds;
    if (attachmentIds.count < 1) {
        return;
    }
    for (NSString *attachmentId in attachmentIds) {
        // We need to fetch each attachment, since [TSAttachment removeWithTransaction:] does important work.
        TSAttachment *_Nullable attachment = [TSAttachment anyFetchWithUniqueId:attachmentId transaction:transaction];
        if (!attachment) {
            OWSLogError(@"Couldn't load interaction's attachment for deletion.");
            continue;
        }
        [attachment anyRemoveWithTransaction:transaction];
    };
}

#pragma mark -

- (TSOutgoingMessageState)messageState
{
    TSOutgoingMessageState newMessageState =
        [TSOutgoingMessage messageStateForRecipientStates:self.recipientAddressStates.allValues];
    if (self.hasLegacyMessageState) {
        if (newMessageState == TSOutgoingMessageStateSent || self.legacyMessageState == TSOutgoingMessageStateSent) {
            return TSOutgoingMessageStateSent;
        }
    }
    return newMessageState;
}

- (BOOL)wasDeliveredToAnyRecipient
{
    if (self.deliveredRecipientAddresses.count > 0) {
        return YES;
    }
    return (self.hasLegacyMessageState && self.legacyWasDelivered && self.messageState == TSOutgoingMessageStateSent);
}

- (BOOL)wasSentToAnyRecipient
{
    if (self.sentRecipientAddresses.count > 0) {
        return YES;
    }
    return (self.hasLegacyMessageState && self.messageState == TSOutgoingMessageStateSent);
}

+ (TSOutgoingMessageState)messageStateForRecipientStates:(NSArray<TSOutgoingMessageRecipientState *> *)recipientStates
{
    OWSAssertDebug(recipientStates);

    // If there are any "sending" recipients, consider this message "sending".
    // If there are any "pending" recipients, consider this message "pending".
    BOOL hasFailed = NO;
    for (TSOutgoingMessageRecipientState *recipientState in recipientStates) {
        if (recipientState.state == OWSOutgoingMessageRecipientStateSending) {
            return TSOutgoingMessageStateSending;
        } else if (recipientState.state == OWSOutgoingMessageRecipientStatePending) {
            return TSOutgoingMessageStatePending;
        } else if (recipientState.state == OWSOutgoingMessageRecipientStateFailed) {
            hasFailed = YES;
        }
    }

    // If there are any "failed" recipients, consider this message "failed".
    if (hasFailed) {
        return TSOutgoingMessageStateFailed;
    }

    // Otherwise, consider the message "sent".
    //
    // NOTE: This includes messages with no recipients.
    return TSOutgoingMessageStateSent;
}

- (BOOL)shouldBeSaved
{
    if (!super.shouldBeSaved) {
        return NO;
    }
    if (self.groupMetaMessage == TSGroupMetaMessageDeliver || self.groupMetaMessage == TSGroupMetaMessageUnspecified) {
        return YES;
    }

    // There's no need to save this message, since it's not displayed to the user.
    //
    // Should we find a need to save this in the future, we need to exclude any non-serializable properties.
    OWSLogDebug(@"Skipping save for transient outgoing message.");
    return NO;
}

- (void)anyWillInsertWithTransaction:(SDSAnyWriteTransaction *)transaction
{
    [super anyWillInsertWithTransaction:transaction];

    _storedMessageState = self.messageState;
}

- (void)anyWillUpdateWithTransaction:(SDSAnyWriteTransaction *)transaction
{
    [super anyWillUpdateWithTransaction:transaction];

    _storedMessageState = self.messageState;
}

// This method will be called after every insert and update, so it needs
// to be cheap.
- (BOOL)shouldStartExpireTimer
{
    if (self.hasPerConversationExpirationStarted) {
        // Expiration already started.
        return YES;
    } else if (!self.hasPerConversationExpiration) {
        return NO;
    } else if (!super.shouldStartExpireTimer) {
        return NO;
    }

    // It's not clear if we should wait until _all_ recipients have reached "sent or later"
    // (which could never occur if one group member is unregistered) or only wait until
    // the first recipient has reached "sent or later" (which could cause partially delivered
    // messages to expire).  For now, we'll do the latter.
    //
    // TODO: Revisit this decision.
    return self.messageState == TSOutgoingMessageStateSent;
}

- (BOOL)isOnline
{
    return NO;
}

- (OWSInteractionType)interactionType
{
    return OWSInteractionType_OutgoingMessage;
}

- (NSArray<SignalServiceAddress *> *)recipientAddresses
{
    return self.recipientAddressStates.allKeys;
}

- (NSArray<SignalServiceAddress *> *)sendingRecipientAddresses
{
    NSMutableArray<SignalServiceAddress *> *result = [NSMutableArray new];
    for (SignalServiceAddress *recipientAddress in self.recipientAddressStates) {
        TSOutgoingMessageRecipientState *recipientState = self.recipientAddressStates[recipientAddress];
        if (recipientState.state == OWSOutgoingMessageRecipientStateSending
            || recipientState.state == OWSOutgoingMessageRecipientStatePending) {
            [result addObject:recipientAddress];
        }
    }
    return result;
}

- (NSArray<SignalServiceAddress *> *)sentRecipientAddresses
{
    NSMutableArray<SignalServiceAddress *> *result = [NSMutableArray new];
    for (SignalServiceAddress *recipientAddress in self.recipientAddressStates) {
        TSOutgoingMessageRecipientState *recipientState = self.recipientAddressStates[recipientAddress];
        if (recipientState.state == OWSOutgoingMessageRecipientStateSent) {
            [result addObject:recipientAddress];
        }
    }
    return result;
}

- (NSArray<SignalServiceAddress *> *)deliveredRecipientAddresses
{
    NSMutableArray<SignalServiceAddress *> *result = [NSMutableArray new];
    for (SignalServiceAddress *recipientAddress in self.recipientAddressStates) {
        TSOutgoingMessageRecipientState *recipientState = self.recipientAddressStates[recipientAddress];
        if (recipientState.deliveryTimestamp != nil) {
            [result addObject:recipientAddress];
        }
    }
    return result;
}

- (NSArray<SignalServiceAddress *> *)readRecipientAddresses
{
    NSMutableArray<SignalServiceAddress *> *result = [NSMutableArray new];
    for (SignalServiceAddress *recipientAddress in self.recipientAddressStates) {
        TSOutgoingMessageRecipientState *recipientState = self.recipientAddressStates[recipientAddress];
        if (recipientState.readTimestamp != nil) {
            [result addObject:recipientAddress];
        }
    }
    return result;
}

- (NSArray<SignalServiceAddress *> *)viewedRecipientAddresses
{
    NSMutableArray<SignalServiceAddress *> *result = [NSMutableArray new];
    for (SignalServiceAddress *recipientAddress in self.recipientAddressStates) {
        TSOutgoingMessageRecipientState *recipientState = self.recipientAddressStates[recipientAddress];
        if (recipientState.viewedTimestamp != nil) {
            [result addObject:recipientAddress];
        }
    }
    return result;
}

- (NSUInteger)sentRecipientsCount
{
    return [self.recipientAddressStates
                .allValues filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(
                                                           TSOutgoingMessageRecipientState *recipientState,
                                                           NSDictionary<NSString *, id> *_Nullable bindings) {
        return recipientState.state == OWSOutgoingMessageRecipientStateSent;
    }]].count;
}

- (nullable TSOutgoingMessageRecipientState *)recipientStateForAddress:(SignalServiceAddress *)address
{
    OWSAssertDebug(address.isValid);

    TSOutgoingMessageRecipientState *_Nullable result = self.recipientAddressStates[address];
    OWSAssertDebug(result);
    return [result copy];
}

#pragma mark - Update With... Methods

- (void)updateWithSendingError:(NSError *)error transaction:(SDSAnyWriteTransaction *)transaction
{
    OWSAssertDebug(error);
    [self
        anyUpdateOutgoingMessageWithTransaction:transaction
                                          block:^(TSOutgoingMessage *message) {
                                              // Mark any "sending" recipients as "failed."
                                              for (TSOutgoingMessageRecipientState *recipientState in message
                                                       .recipientAddressStates.allValues) {
                                                  if (recipientState.state == OWSOutgoingMessageRecipientStateSending) {
                                                      recipientState.state = OWSOutgoingMessageRecipientStateFailed;
                                                  }
                                              }
                                              [message setMostRecentFailureText:error.localizedDescription];
                                          }];
}

- (void)updateWithAllSendingRecipientsMarkedAsFailedWithTansaction:(SDSAnyWriteTransaction *)transaction
{
    OWSAssertDebug(transaction);

    [self
        anyUpdateOutgoingMessageWithTransaction:transaction
                                          block:^(TSOutgoingMessage *message) {
                                              // Mark any "sending" recipients as "failed."
                                              for (TSOutgoingMessageRecipientState *recipientState in message
                                                       .recipientAddressStates.allValues) {
                                                  if (recipientState.state == OWSOutgoingMessageRecipientStateSending) {
                                                      recipientState.state = OWSOutgoingMessageRecipientStateFailed;
                                                  }
                                              }
                                          }];
}

- (BOOL)hasFailedRecipients
{
    for (TSOutgoingMessageRecipientState *recipientState in self.recipientAddressStates.allValues) {
        if (recipientState.state == OWSOutgoingMessageRecipientStateFailed) {
            return YES;
        }
    }
    return NO;
}

- (void)updateAllUnsentRecipientsAsSendingWithTransaction:(SDSAnyWriteTransaction *)transaction
{
    OWSAssertDebug(transaction);

    [self
        anyUpdateOutgoingMessageWithTransaction:transaction
                                          block:^(TSOutgoingMessage *message) {
                                              // Mark any "failed" recipients as "sending."
                                              for (TSOutgoingMessageRecipientState *recipientState in message
                                                       .recipientAddressStates.allValues) {
                                                  if (recipientState.state == OWSOutgoingMessageRecipientStateFailed) {
                                                      recipientState.state = OWSOutgoingMessageRecipientStateSending;
                                                  }
                                              }
                                          }];
}

- (void)updateWithHasSyncedTranscript:(BOOL)hasSyncedTranscript transaction:(SDSAnyWriteTransaction *)transaction
{
    [self anyUpdateOutgoingMessageWithTransaction:transaction
                                            block:^(TSOutgoingMessage *message) {
                                                [message setHasSyncedTranscript:hasSyncedTranscript];
                                            }];
}

- (void)updateWithSentRecipient:(SignalServiceAddress *)recipientAddress
                    wasSentByUD:(BOOL)wasSentByUD
                    transaction:(SDSAnyWriteTransaction *)transaction
{
    OWSAssertDebug(recipientAddress.isValid);
    OWSAssertDebug(transaction);

    [self anyUpdateOutgoingMessageWithTransaction:transaction
                                            block:^(TSOutgoingMessage *message) {
                                                TSOutgoingMessageRecipientState *_Nullable recipientState
                                                    = message.recipientAddressStates[recipientAddress];
                                                if (!recipientState) {
                                                    OWSFailDebug(
                                                        @"Missing recipient state for recipient: %@", recipientAddress);
                                                    return;
                                                }
                                                recipientState.state = OWSOutgoingMessageRecipientStateSent;
                                                recipientState.wasSentByUD = wasSentByUD;
                                                recipientState.errorCode = nil;
                                            }];
}

- (void)updateWithSkippedRecipient:(SignalServiceAddress *)recipientAddress
                       transaction:(SDSAnyWriteTransaction *)transaction
{
    OWSAssertDebug(recipientAddress.isValid);
    OWSAssertDebug(transaction);

    [self anyUpdateOutgoingMessageWithTransaction:transaction
                                            block:^(TSOutgoingMessage *message) {
                                                TSOutgoingMessageRecipientState *_Nullable recipientState
                                                    = message.recipientAddressStates[recipientAddress];
                                                if (!recipientState) {
                                                    OWSFailDebug(
                                                        @"Missing recipient state for recipient: %@", recipientAddress);
                                                    return;
                                                }
                                                recipientState.state = OWSOutgoingMessageRecipientStateSkipped;
                                            }];
}

- (void)updateWithFailedRecipient:(SignalServiceAddress *)recipientAddress
                            error:(NSError *)error
                      transaction:(SDSAnyWriteTransaction *)transaction
{
    OWSAssertDebug(recipientAddress.isValid);
    OWSAssertDebug(transaction);

    OWSLogWarn(@"Send to recipient failed, address: %@, timestamp: %llu, error: %@ (isRetryable: %d)",
        recipientAddress,
        self.timestamp,
        error,
        error.isRetryable);

    [self
        anyUpdateOutgoingMessageWithTransaction:transaction
                                          block:^(TSOutgoingMessage *message) {
                                              TSOutgoingMessageRecipientState *_Nullable recipientState
                                                  = message.recipientAddressStates[recipientAddress];
                                              if (!recipientState) {
                                                  OWSFailDebug(
                                                      @"Missing recipient state for recipient: %@", recipientAddress);
                                                  return;
                                              }

                                              if ([error
                                                      ows_isSSKErrorWithCode:OWSErrorCodeServerRejectedSuspectedSpam]) {
                                                  recipientState.state = OWSOutgoingMessageRecipientStatePending;
                                              } else {
                                                  recipientState.state = OWSOutgoingMessageRecipientStateFailed;
                                              }
                                              recipientState.errorCode = @(error.code);
                                          }];
}

- (void)updateWithDeliveredRecipient:(SignalServiceAddress *)recipientAddress
                   deliveryTimestamp:(NSNumber *_Nullable)deliveryTimestamp
                         transaction:(SDSAnyWriteTransaction *)transaction
{
    OWSAssertDebug(recipientAddress.isValid);
    OWSAssertDebug(transaction);

    // Ignore receipts for messages that have been deleted.
    // They are no longer relevant to this message.
    if (self.wasRemotelyDeleted) {
        return;
    }

    // If delivery notification doesn't include timestamp, use "now" as an estimate.
    if (!deliveryTimestamp) {
        deliveryTimestamp = @([NSDate ows_millisecondTimeStamp]);
    }

    [self anyUpdateOutgoingMessageWithTransaction:transaction
                                            block:^(TSOutgoingMessage *message) {
                                                TSOutgoingMessageRecipientState *_Nullable recipientState
                                                    = message.recipientAddressStates[recipientAddress];
                                                if (!recipientState) {
                                                    OWSFailDebug(@"Missing recipient state for delivered recipient: %@",
                                                        recipientAddress);
                                                    return;
                                                }
                                                if (recipientState.state != OWSOutgoingMessageRecipientStateSent) {
                                                    OWSLogWarn(@"marking unsent message as delivered.");
                                                }
                                                recipientState.state = OWSOutgoingMessageRecipientStateSent;
                                                recipientState.deliveryTimestamp = deliveryTimestamp;
                                                recipientState.errorCode = nil;
                                            }];
}

- (void)updateWithReadRecipient:(SignalServiceAddress *)recipientAddress
                  readTimestamp:(uint64_t)readTimestamp
                    transaction:(SDSAnyWriteTransaction *)transaction
{
    OWSAssertDebug(recipientAddress.isValid);
    OWSAssertDebug(transaction);

    // Ignore receipts for messages that have been deleted.
    // They are no longer relevant to this message.
    if (self.wasRemotelyDeleted) {
        return;
    }

    [self anyUpdateOutgoingMessageWithTransaction:transaction
                                            block:^(TSOutgoingMessage *message) {
                                                TSOutgoingMessageRecipientState *_Nullable recipientState
                                                    = message.recipientAddressStates[recipientAddress];
                                                if (!recipientState) {
                                                    OWSFailDebug(@"Missing recipient state for delivered recipient: %@",
                                                        recipientAddress);
                                                    return;
                                                }
                                                if (recipientState.state != OWSOutgoingMessageRecipientStateSent) {
                                                    OWSLogWarn(@"marking unsent message as delivered.");
                                                }
                                                recipientState.state = OWSOutgoingMessageRecipientStateSent;
                                                recipientState.readTimestamp = @(readTimestamp);
                                                recipientState.errorCode = nil;
                                            }];
}

- (void)updateWithViewedRecipient:(SignalServiceAddress *)recipientAddress
                  viewedTimestamp:(uint64_t)viewedTimestamp
                      transaction:(SDSAnyWriteTransaction *)transaction
{
    OWSAssertDebug(recipientAddress.isValid);
    OWSAssertDebug(transaction);

    // Ignore receipts for messages that have been deleted.
    // They are no longer relevant to this message.
    if (self.wasRemotelyDeleted) {
        return;
    }

    [self anyUpdateOutgoingMessageWithTransaction:transaction
                                            block:^(TSOutgoingMessage *message) {
                                                TSOutgoingMessageRecipientState *_Nullable recipientState
                                                    = message.recipientAddressStates[recipientAddress];
                                                if (!recipientState) {
                                                    OWSFailDebug(@"Missing recipient state for delivered recipient: %@",
                                                        recipientAddress);
                                                    return;
                                                }
                                                if (recipientState.state != OWSOutgoingMessageRecipientStateSent) {
                                                    OWSLogWarn(@"marking unsent message as delivered.");
                                                }
                                                recipientState.state = OWSOutgoingMessageRecipientStateSent;
                                                recipientState.viewedTimestamp = @(viewedTimestamp);
                                                recipientState.errorCode = nil;
                                            }];
}

- (void)updateWithWasSentFromLinkedDeviceWithUDRecipientAddresses:
            (nullable NSArray<SignalServiceAddress *> *)udRecipientAddresses
                                          nonUdRecipientAddresses:
                                              (nullable NSArray<SignalServiceAddress *> *)nonUdRecipientAddresses
                                                     isSentUpdate:(BOOL)isSentUpdate
                                                      transaction:(SDSAnyWriteTransaction *)transaction
{
    OWSAssertDebug(transaction);

    [self
        anyUpdateOutgoingMessageWithTransaction:transaction
                                          block:^(TSOutgoingMessage *message) {
                                              if (udRecipientAddresses.count > 0 || nonUdRecipientAddresses.count > 0) {
                                                  // If we have specific recipient info from the transcript,
                                                  // build a new recipient state map.
                                                  NSMutableDictionary<SignalServiceAddress *,
                                                      TSOutgoingMessageRecipientState *> *recipientAddressStates
                                                      = [NSMutableDictionary new];
                                                  for (SignalServiceAddress *recipientAddress in udRecipientAddresses) {
                                                      if (!recipientAddress.isValid) {
                                                          OWSFailDebug(@"Ignoring invalid address.");
                                                          continue;
                                                      }
                                                      if (recipientAddressStates[recipientAddress]) {
                                                          OWSFailDebug(@"recipient appears more than once in recipient "
                                                                       @"lists: %@",
                                                              recipientAddress);
                                                          continue;
                                                      }
                                                      TSOutgoingMessageRecipientState *recipientState =
                                                          [TSOutgoingMessageRecipientState new];
                                                      recipientState.state = OWSOutgoingMessageRecipientStateSent;
                                                      recipientState.wasSentByUD = YES;
                                                      recipientAddressStates[recipientAddress] = recipientState;
                                                  }
                                                  for (SignalServiceAddress
                                                           *recipientAddress in nonUdRecipientAddresses) {
                                                      if (!recipientAddress.isValid) {
                                                          OWSFailDebug(@"Ignoring invalid address.");
                                                          continue;
                                                      }
                                                      if (recipientAddressStates[recipientAddress]) {
                                                          OWSFailDebug(@"recipient appears more than once in recipient "
                                                                       @"lists: %@",
                                                              recipientAddress);
                                                          continue;
                                                      }
                                                      TSOutgoingMessageRecipientState *recipientState =
                                                          [TSOutgoingMessageRecipientState new];
                                                      recipientState.state = OWSOutgoingMessageRecipientStateSent;
                                                      recipientState.wasSentByUD = NO;
                                                      recipientAddressStates[recipientAddress] = recipientState;
                                                  }

                                                  if (isSentUpdate) {
                                                      // If this is a "sent update", make sure that:
                                                      //
                                                      // a) "Sent updates" should never remove any recipients.  We end
                                                      // up with the
                                                      //    union of the existing and new recipients.
                                                      // b) "Sent updates" should never downgrade the "recipient state"
                                                      // for any
                                                      //    recipients.  Prefer existing "recipient state"; "sent
                                                      //    updates" only add new recipients at the "sent" state.
                                                      //
                                                      // Therefore we retain all existing entries in the recipient state
                                                      // map.
                                                      [recipientAddressStates
                                                          addEntriesFromDictionary:self.recipientAddressStates];
                                                  }

                                                  [message setRecipientAddressStates:recipientAddressStates];
                                              } else {
                                                  // Otherwise assume this is a legacy message before UD was introduced,
                                                  // and mark any "sending" recipient as "sent".  Note that this will
                                                  // apply to non-legacy messages with no recipients.
                                                  for (TSOutgoingMessageRecipientState *recipientState in message
                                                           .recipientAddressStates.allValues) {
                                                      if (recipientState.state
                                                          == OWSOutgoingMessageRecipientStateSending) {
                                                          recipientState.state = OWSOutgoingMessageRecipientStateSent;
                                                      }
                                                  }
                                              }

                                              if (!isSentUpdate) {
                                                  [message setIsFromLinkedDevice:YES];
                                              }
                                          }];
}

- (void)updateWithSendingToSingleGroupRecipient:(SignalServiceAddress *)singleGroupRecipient
                                    transaction:(SDSAnyWriteTransaction *)transaction
{
    OWSAssertDebug(transaction);
    OWSAssertDebug(singleGroupRecipient.isValid);

    [self anyUpdateOutgoingMessageWithTransaction:transaction
                                            block:^(TSOutgoingMessage *message) {
                                                if (!singleGroupRecipient.isValid) {
                                                    OWSFailDebug(@"Ignoring invalid address.");
                                                    return;
                                                }
                                                TSOutgoingMessageRecipientState *recipientState =
                                                    [TSOutgoingMessageRecipientState new];
                                                recipientState.state = OWSOutgoingMessageRecipientStateSending;
                                                [message setRecipientAddressStates:@{
                                                    singleGroupRecipient : recipientState,
                                                }];
                                            }];
}

- (nullable NSNumber *)firstRecipientReadTimestamp
{
    NSNumber *result = nil;
    for (TSOutgoingMessageRecipientState *recipientState in self.recipientAddressStates.allValues) {
        if (!recipientState.readTimestamp) {
            continue;
        }
        if (!result || (result.unsignedLongLongValue > recipientState.readTimestamp.unsignedLongLongValue)) {
            result = recipientState.readTimestamp;
        }
    }
    return result;
}

- (void)updateWithRecipientAddressStates:
            (nullable NSDictionary<SignalServiceAddress *, TSOutgoingMessageRecipientState *> *)recipientAddressStates
                             transaction:(SDSAnyWriteTransaction *)transaction
{
    [self anyUpdateOutgoingMessageWithTransaction:transaction
                                            block:^(TSOutgoingMessage *message) {
                                                message.recipientAddressStates = [recipientAddressStates copy];
                                            }];
}

#ifdef TESTABLE_BUILD
- (void)updateWithFakeMessageState:(TSOutgoingMessageState)messageState
                       transaction:(SDSAnyWriteTransaction *)transaction
{
    OWSAssertDebug(transaction);
    [self anyUpdateOutgoingMessageWithTransaction:transaction
                                            block:^(TSOutgoingMessage *message) {
                                                for (TSOutgoingMessageRecipientState *recipientState in message
                                                         .recipientAddressStates.allValues) {
                                                    switch (messageState) {
                                                        case TSOutgoingMessageStateSending:
                                                            recipientState.state
                                                                = OWSOutgoingMessageRecipientStateSending;
                                                            break;
                                                        case TSOutgoingMessageStateFailed:
                                                            recipientState.state
                                                                = OWSOutgoingMessageRecipientStateFailed;
                                                            break;
                                                        case TSOutgoingMessageStateSent:
                                                            recipientState.state = OWSOutgoingMessageRecipientStateSent;
                                                            break;
                                                        case TSOutgoingMessageStatePending:
                                                            recipientState.state
                                                                = OWSOutgoingMessageRecipientStatePending;
                                                            break;
                                                        default:
                                                            OWSFailDebug(@"unexpected message state.");
                                                            break;
                                                    }
                                                }
                                            }];
}
#endif

#pragma mark -

- (nullable SSKProtoDataMessageBuilder *)dataMessageBuilderWithThread:(TSThread *)thread
                                                          transaction:(SDSAnyReadTransaction *)transaction
{
    OWSAssertDebug(thread);

    SSKProtoDataMessageBuilder *builder = [SSKProtoDataMessage builder];
    [builder setTimestamp:self.timestamp];

    NSUInteger requiredProtocolVersion = SSKProtoDataMessageProtocolVersionInitial;

    if (self.isViewOnceMessage) {
        [builder setIsViewOnce:YES];
        requiredProtocolVersion = SSKProtoDataMessageProtocolVersionViewOnceVideo;
    }

    if ([self.body lengthOfBytesUsingEncoding:NSUTF8StringEncoding] <= kOversizeTextMessageSizeThreshold) {
        [builder setBody:self.body];
    } else {
        OWSFailDebug(@"message body length too long.");
        NSString *truncatedBody = [self.body copy];
        while ([truncatedBody lengthOfBytesUsingEncoding:NSUTF8StringEncoding] > kOversizeTextMessageSizeThreshold) {
            OWSLogError(@"truncating body which is too long: %lu",
                (unsigned long)[truncatedBody lengthOfBytesUsingEncoding:NSUTF8StringEncoding]);
            truncatedBody = [truncatedBody substringToIndex:truncatedBody.length / 2];
        }
        [builder setBody:truncatedBody];
    }

    NSArray<SSKProtoDataMessageBodyRange *> *bodyRanges = [self bodyRangeProtosWithBodyText:self.body
                                                                              andBodyRanges:self.bodyRanges];
    if (bodyRanges.count > 0) {
        [builder setBodyRanges:bodyRanges];

        if (requiredProtocolVersion < SSKProtoDataMessageProtocolVersionMentions) {
            requiredProtocolVersion = SSKProtoDataMessageProtocolVersionMentions;
        }
    }

    [builder setExpireTimer:self.expiresInSeconds];
    
    // Group Messages
    BOOL attachmentWasGroupAvatar = NO;
    if ([thread isKindOfClass:[TSGroupThread class]]) {
        TSGroupThread *groupThread = (TSGroupThread *)thread;
        OutgoingGroupProtoResult result;
        switch (groupThread.groupModel.groupsVersion) {
            case GroupsVersionV1:
                result = [self addGroupsV1ToDataMessageBuilder:builder groupThread:groupThread transaction:transaction];
                break;
            case GroupsVersionV2:
                result = [self addGroupsV2ToDataMessageBuilder:builder groupThread:groupThread transaction:transaction];
                break;
        }
        switch (result) {
            case OutgoingGroupProtoResult_Error:
                return nil;
            case OutgoingGroupProtoResult_AddedWithGroupAvatar:
                attachmentWasGroupAvatar = YES;
                break;
            case OutgoingGroupProtoResult_AddedWithoutGroupAvatar:
                break;
        }
    }
    
    // Message Attachments
    if (!attachmentWasGroupAvatar) {
        NSMutableArray<SSKProtoAttachmentPointer *> *attachments = [NSMutableArray new];
        for (NSString *attachmentId in self.attachmentIds) {
            SSKProtoAttachmentPointer *_Nullable attachmentProto =
                [TSAttachmentStream buildProtoForAttachmentId:attachmentId transaction:transaction];
            if (!attachmentProto) {
                OWSFailDebug(@"could not build protobuf.");
                return nil;
            }
            [attachments addObject:attachmentProto];
            if (requiredProtocolVersion < SSKProtoDataMessageProtocolVersionCdnSelectorAttachments
                && (attachmentProto.cdnKey.length > 0 || attachmentProto.cdnNumber > 0)) {
                requiredProtocolVersion = SSKProtoDataMessageProtocolVersionCdnSelectorAttachments;
            }
        }
        [builder setAttachments:attachments];
    }

    // Quoted Reply
    SSKProtoDataMessageQuoteBuilder *_Nullable quotedMessageBuilder =
        [self quotedMessageBuilderWithTransaction:transaction];
    if (quotedMessageBuilder) {
        NSError *error;
        SSKProtoDataMessageQuote *_Nullable quoteProto = [quotedMessageBuilder buildAndReturnError:&error];
        if (error || !quoteProto) {
            OWSFailDebug(@"could not build protobuf: %@.", error);
            return nil;
        }
        [builder setQuote:quoteProto];

        if (quoteProto.bodyRanges.count > 0) {
            if (requiredProtocolVersion < SSKProtoDataMessageProtocolVersionMentions) {
                requiredProtocolVersion = SSKProtoDataMessageProtocolVersionMentions;
            }
        }
    }

    // Contact Share
    if (self.contactShare) {
        SSKProtoDataMessageContact *_Nullable contactProto = [OWSContacts protoForContact:self.contactShare
                                                                              transaction:transaction];
        if (contactProto) {
            [builder addContact:contactProto];
        } else {
            OWSFailDebug(@"contactProto was unexpectedly nil");
        }
    }

    // Link Preview
    if (self.linkPreview) {
        SSKProtoDataMessagePreviewBuilder *previewBuilder =
            [SSKProtoDataMessagePreview builderWithUrl:self.linkPreview.urlString];
        if (self.linkPreview.title.length > 0) {
            [previewBuilder setTitle:self.linkPreview.title];
        }
        if (self.linkPreview.imageAttachmentId) {
            SSKProtoAttachmentPointer *_Nullable attachmentProto =
                [TSAttachmentStream buildProtoForAttachmentId:self.linkPreview.imageAttachmentId
                                                  transaction:transaction];
            if (!attachmentProto) {
                OWSFailDebug(@"Could not build link preview image protobuf.");
            } else {
                [previewBuilder setImage:attachmentProto];
            }
        }
        if (self.linkPreview.date) {
            uint64_t interval = [self.linkPreview.date ows_millisecondsSince1970];
            [previewBuilder setDate:interval];
        }
        if (self.linkPreview.previewDescription) {
            [previewBuilder setPreviewDescription:self.linkPreview.previewDescription];
        }

        NSError *error;
        SSKProtoDataMessagePreview *_Nullable previewProto = [previewBuilder buildAndReturnError:&error];
        if (error || !previewProto) {
            OWSFailDebug(@"Could not build link preview protobuf: %@.", error);
        } else {
            [builder addPreview:previewProto];
        }
    }

    // Sticker
    if (self.messageSticker) {
        SSKProtoAttachmentPointer *_Nullable attachmentProto =
            [TSAttachmentStream buildProtoForAttachmentId:self.messageSticker.attachmentId transaction:transaction];
        if (!attachmentProto) {
            OWSFailDebug(@"Could not build sticker attachment protobuf.");
        } else {
            SSKProtoDataMessageStickerBuilder *stickerBuilder =
                [SSKProtoDataMessageSticker builderWithPackID:self.messageSticker.packId
                                                      packKey:self.messageSticker.packKey
                                                    stickerID:self.messageSticker.stickerId
                                                         data:attachmentProto];
            if (self.messageSticker.emoji.length > 0) {
                [stickerBuilder setEmoji:self.messageSticker.emoji];
            }

            NSError *error;
            SSKProtoDataMessageSticker *_Nullable stickerProto = [stickerBuilder buildAndReturnError:&error];
            if (error || !stickerProto) {
                OWSFailDebug(@"Could not build sticker protobuf: %@.", error);
            } else {
                [builder setSticker:stickerProto];
            }
        }
    }

    [builder setRequiredProtocolVersion:(uint32_t)requiredProtocolVersion];
    return builder;
}

- (OutgoingGroupProtoResult)addGroupsV1ToDataMessageBuilder:(SSKProtoDataMessageBuilder *)builder
                                                groupThread:(TSGroupThread *)groupThread
                                                transaction:(SDSAnyReadTransaction *)transaction
{
    OWSAssertDebug(builder);
    OWSAssertDebug(groupThread);
    OWSAssertDebug(transaction);

    TSGroupModel *groupModel = groupThread.groupModel;
    OWSAssertDebug(groupModel.groupsVersion == GroupsVersionV1);

    SSKProtoGroupContextType groupMessageType;
    switch (self.groupMetaMessage) {
        case TSGroupMetaMessageQuit:
            groupMessageType = SSKProtoGroupContextTypeQuit;
            break;
        case TSGroupMetaMessageUpdate:
        case TSGroupMetaMessageNew:
            groupMessageType = SSKProtoGroupContextTypeUpdate;
            break;
        default:
            groupMessageType = SSKProtoGroupContextTypeDeliver;
            break;
    }
    BOOL attachmentWasGroupAvatar = NO;
    SSKProtoGroupContextBuilder *groupBuilder = [SSKProtoGroupContext builderWithId:groupModel.groupId];
    [groupBuilder setType:groupMessageType];
    if (groupMessageType == SSKProtoGroupContextTypeUpdate) {
        if (groupModel.groupAvatarData != nil && self.attachmentIds.count == 1) {
            attachmentWasGroupAvatar = YES;
            SSKProtoAttachmentPointer *_Nullable attachmentProto =
                [TSAttachmentStream buildProtoForAttachmentId:self.attachmentIds.firstObject transaction:transaction];
            if (!attachmentProto) {
                OWSFailDebug(@"could not build protobuf.");
                return OutgoingGroupProtoResult_Error;
            }
            [groupBuilder setAvatar:attachmentProto];
        }

        NSMutableArray<NSString *> *membersE164 = [NSMutableArray new];
        NSMutableArray<SSKProtoGroupContextMember *> *members = [NSMutableArray new];

        for (SignalServiceAddress *address in groupModel.groupMembers) {
            if (address.phoneNumber) {
                [membersE164 addObject:address.phoneNumber];

                // Newer desktops only know how to handle the "pairing"
                // fields that we rolled back when implementing UUID
                // trust. We need to continue populating them with
                // phone number only to make sure desktop can see
                // group membership.
                SSKProtoGroupContextMemberBuilder *memberBuilder = [SSKProtoGroupContextMember builder];
                memberBuilder.e164 = address.phoneNumber;

                NSError *error;
                SSKProtoGroupContextMember *_Nullable member = [memberBuilder buildAndReturnError:&error];
                if (error || !member) {
                    OWSFailDebug(@"could not build members protobuf: %@", error);
                } else {
                    [members addObject:member];
                }
            } else {
                OWSFailDebug(@"Unexpectedly have a UUID only member in a v1 group, ignoring %@", address);
            }
        }

        if (!SSKFeatureFlags.phoneNumberSharing || SSKDebugFlags.allowV1GroupsUpdates) {
            [groupBuilder setMembersE164:membersE164];
            [groupBuilder setMembers:members];
        } else {
            OWSFailDebug(@"Groups v1 must be discontinued before we roll out phone number sharing settings");
        }

        [groupBuilder setName:groupModel.groupName];
    }
    NSError *error;
    SSKProtoGroupContext *_Nullable groupContextProto = [groupBuilder buildAndReturnError:&error];
    if (error || !groupContextProto) {
        OWSFailDebug(@"could not build protobuf: %@.", error);
        return OutgoingGroupProtoResult_Error;
    }
    [builder setGroup:groupContextProto];

    return (attachmentWasGroupAvatar ? OutgoingGroupProtoResult_AddedWithGroupAvatar
                                     : OutgoingGroupProtoResult_AddedWithoutGroupAvatar);
}

- (OutgoingGroupProtoResult)addGroupsV2ToDataMessageBuilder:(SSKProtoDataMessageBuilder *)builder
                                                groupThread:(TSGroupThread *)groupThread
                                                transaction:(SDSAnyReadTransaction *)transaction
{
    OWSAssertDebug(builder);
    OWSAssertDebug(groupThread);
    OWSAssertDebug(transaction);

    if (![groupThread.groupModel isKindOfClass:[TSGroupModelV2 class]]) {
        OWSFailDebug(@"Invalid group model.");
        return OutgoingGroupProtoResult_Error;
    }
    TSGroupModelV2 *groupModel = (TSGroupModelV2 *)groupThread.groupModel;

    NSError *error;
    SSKProtoGroupContextV2 *_Nullable groupContextV2 =
        [self.groupsV2 buildGroupContextV2ProtoWithGroupModel:groupModel
                                       changeActionsProtoData:self.changeActionsProtoData
                                                        error:&error];
    if (groupContextV2 == nil || error != nil) {
        OWSFailDebug(@"Error: %@", error);
        return OutgoingGroupProtoResult_Error;
    }
    [builder setGroupV2:groupContextV2];
    return OutgoingGroupProtoResult_AddedWithoutGroupAvatar;
}

- (NSArray<SSKProtoDataMessageBodyRange *> *)bodyRangeProtosWithBodyText:(NSString *)bodyText
                                                           andBodyRanges:(nullable MessageBodyRanges *)bodyRanges
{
    if (bodyText.length == 0 || bodyRanges == nil) {
        return @[];
    }

    NSMutableArray<SSKProtoDataMessageBodyRange *> *bodyRangeProtos = [NSMutableArray new];
    for (NSValue *rangeValue in bodyRanges.mentions) {
        NSRange range = [rangeValue rangeValue];
        NSUUID *uuid = bodyRanges.mentions[rangeValue];

        if (range.location + range.length > bodyText.length) {
            OWSFailDebug(@"Skipping invalid range in body ranges.");
            continue;
        }

        SSKProtoDataMessageBodyRangeBuilder *bodyRangeBuilder = [SSKProtoDataMessageBodyRange builder];
        [bodyRangeBuilder setStart:(uint32_t)range.location];
        [bodyRangeBuilder setLength:(uint32_t)range.length];
        [bodyRangeBuilder setMentionUuid:uuid.UUIDString];

        NSError *error;
        SSKProtoDataMessageBodyRange *_Nullable bodyRange = [bodyRangeBuilder buildAndReturnError:&error];
        if (!bodyRange || error) {
            OWSFailDebug(@"could not build protobuf: %@", error);
            return nil;
        }

        [bodyRangeProtos addObject:bodyRange];
    }
    return [bodyRangeProtos copy];
}

- (nullable SSKProtoDataMessageQuoteBuilder *)quotedMessageBuilderWithTransaction:(SDSAnyReadTransaction *)transaction
{
    if (!self.quotedMessage) {
        return nil;
    }
    TSQuotedMessage *quotedMessage = self.quotedMessage;

    SSKProtoDataMessageQuoteBuilder *quoteBuilder = [SSKProtoDataMessageQuote builderWithId:quotedMessage.timestamp];

    if (quotedMessage.authorAddress.phoneNumber && !SSKFeatureFlags.phoneNumberSharing) {
        quoteBuilder.authorE164 = quotedMessage.authorAddress.phoneNumber;
    }

    if (quotedMessage.authorAddress.uuidString) {
        quoteBuilder.authorUuid = quotedMessage.authorAddress.uuidString;
    } else {
        OWSAssertDebug(!SSKFeatureFlags.phoneNumberSharing);
    }

    BOOL hasQuotedText = NO;
    BOOL hasQuotedAttachment = NO;
    if (self.quotedMessage.body.length > 0) {
        hasQuotedText = YES;
        [quoteBuilder setText:quotedMessage.body];

        NSArray<SSKProtoDataMessageBodyRange *> *bodyRanges =
            [self bodyRangeProtosWithBodyText:self.quotedMessage.body andBodyRanges:self.quotedMessage.bodyRanges];
        if (bodyRanges.count > 0) {
            [quoteBuilder setBodyRanges:bodyRanges];
        }
    }

    if (quotedMessage.quotedAttachments) {
        for (OWSAttachmentInfo *attachment in quotedMessage.quotedAttachments) {
            hasQuotedAttachment = YES;

            SSKProtoDataMessageQuoteQuotedAttachmentBuilder *quotedAttachmentBuilder =
                [SSKProtoDataMessageQuoteQuotedAttachment builder];

            quotedAttachmentBuilder.contentType = attachment.contentType;
            quotedAttachmentBuilder.fileName = attachment.sourceFilename;
            if (attachment.thumbnailAttachmentStreamId) {
                quotedAttachmentBuilder.thumbnail =
                    [TSAttachmentStream buildProtoForAttachmentId:attachment.thumbnailAttachmentStreamId
                                                      transaction:transaction];
            }

            NSError *error;
            SSKProtoDataMessageQuoteQuotedAttachment *_Nullable quotedAttachmentMessage =
                [quotedAttachmentBuilder buildAndReturnError:&error];
            if (error || !quotedAttachmentMessage) {
                OWSFailDebug(@"could not build protobuf: %@", error);
                return nil;
            }

            [quoteBuilder addAttachments:quotedAttachmentMessage];
        }
    }

    if (hasQuotedText || hasQuotedAttachment) {
        return quoteBuilder;
    } else {
        OWSFailDebug(@"Invalid quoted message data.");
        return nil;
    }
}

// recipientId is nil when building "sent" sync messages for messages sent to groups.
- (nullable SSKProtoDataMessage *)buildDataMessage:(SignalServiceAddress *_Nullable)address
                                            thread:(TSThread *)thread
                                       transaction:(SDSAnyReadTransaction *)transaction
{
    OWSAssertDebug(thread);
    OWSAssertDebug([thread.uniqueId isEqualToString:self.uniqueThreadId]);
    SSKProtoDataMessageBuilder *_Nullable builder = [self dataMessageBuilderWithThread:thread transaction:transaction];
    if (!builder) {
        OWSFailDebug(@"could not build protobuf.");
        return nil;
    }

    [ProtoUtils addLocalProfileKeyIfNecessary:thread
                                      address:address
                           dataMessageBuilder:builder
                                  transaction:transaction];

    NSError *error;
    SSKProtoDataMessage *_Nullable dataProto = [builder buildAndReturnError:&error];
    if (error || !dataProto) {
        OWSFailDebug(@"could not build protobuf: %@", error);
        return nil;
    }
    return dataProto;
}

- (nullable NSData *)buildPlainTextData:(SignalServiceAddress *)address
                                 thread:(TSThread *)thread
                            transaction:(SDSAnyReadTransaction *)transaction
{
    NSError *error;
    SSKProtoDataMessage *_Nullable dataMessage = [self buildDataMessage:address thread:thread transaction:transaction];
    if (error || !dataMessage) {
        OWSFailDebug(@"could not build protobuf: %@", error);
        return nil;
    }

    SSKProtoContentBuilder *contentBuilder = [SSKProtoContent builder];
    [contentBuilder setDataMessage:dataMessage];
    NSData *_Nullable contentData = [contentBuilder buildSerializedDataAndReturnError:&error];
    if (error || !contentData) {
        OWSFailDebug(@"could not serialize protobuf: %@", error);
        return nil;
    }
    return contentData;
}

- (BOOL)shouldSyncTranscript
{
    return YES;
}

- (NSString *)statusDescription
{
    NSMutableString *result = [NSMutableString new];
    [result appendFormat:@"[status: %@\n", NSStringForOutgoingMessageState(self.messageState)];
    for (SignalServiceAddress *address in self.recipientAddressStates) {
        TSOutgoingMessageRecipientState *recipientState = self.recipientAddressStates[address];
        [result appendFormat:@", %@: %@\n", address, NSStringForOutgoingMessageRecipientState(recipientState.state)];
    }
    [result appendString:@"]"];
    return [result copy];
}

@end

NS_ASSUME_NONNULL_END
